﻿using System;
using System.IO;
using System.Threading;
using System.Reflection;
using System.Xml.Serialization;
using System.Collections.Generic;

using HaroModuleLib;
using Haro.Speech;

namespace Haro.Core
{
    public delegate void ModuleEventHandler(string ModuleName);

    /// <summary>
    /// Allows for the hosting of a module server that load, unload and interact with
    /// identified modules.
    /// </summary>
    internal class ModuleHost: IModuleHost
    {
        private SortedDictionary<string, LoadedModule> m_LoadedModules;
        private SortedDictionary<string, string> m_NonloadedModules;

        public event ModuleEventHandler ModuleGrammarLoaded;
        public event ModuleEventHandler ModuleGrammarUnloaded;
        public event ModuleEventHandler ModuleGrammarReloaded;
        
        #region Constructors
        /// <summary>
        /// Creates a new instance of the module host and automatically scans and loads
        /// any recognizable modules.
        /// </summary>
        public ModuleHost()
        {
            m_LoadedModules = new SortedDictionary<string, LoadedModule>();
            m_NonloadedModules = new SortedDictionary<string, string>();

            ScanForNewModules();
        }

        /// <summary>
        /// Creates a new instance of the module host which adds only the specified modules.
        /// </summary>
        /// <param name="ModuleLocations">Array of strings containing module locations.</param>
        public ModuleHost(string[] ModuleNames)
        {
            m_LoadedModules = new SortedDictionary<string, LoadedModule>();
            m_NonloadedModules = new SortedDictionary<string, string>();

            ScanForNewModules();

            foreach(string s in ModuleNames)
            {
                if (m_NonloadedModules.ContainsKey(s))
                    LoadModule(s);
            }

        }
        #endregion
        
        #region ScanForNewModules
        /// <summary>
        /// Scans for all modules that can be loaded, records them but doesn't load them.
        /// </summary>
        public void ScanForNewModules()
        {
            foreach (string dirs in Directory.GetDirectories(Globals.ModulesDirectory))
            {
                foreach (string file in Directory.GetFiles(dirs, "*.dll"))
                {
                    Debug.Output("Scanning for modules in: " + file);
                    Assembly moduleAssembly = Assembly.LoadFrom(file);

                    try
                    {
                        foreach (Type t in moduleAssembly.GetExportedTypes())
                        {
                            if (t != null && !t.IsAbstract)
                            {
                                Type typeInterface = t.GetInterface(Globals.InterfaceName, true);
                                if (typeInterface != null)
                                {
                                    IModule mod = (IModule)Activator.CreateInstance(moduleAssembly.GetType(t.ToString()));
                                    if (!m_LoadedModules.ContainsKey(mod.Name))
                                        m_NonloadedModules.Add(mod.Name, file);
                                }
                            }
                        }
                    }
                    catch (Exception) { Debug.Output("Failed while scanning dll: " + file); }
                }
            }

            if (m_NonloadedModules.Count != 1)
                Debug.Output(m_NonloadedModules.Count + " new modules found through scanning.");
            else Debug.Output(m_NonloadedModules.Count + " new module found through scanning.");
        }
        #endregion

        #region LoadAllModules
        /// <summary>
        /// Automatically scans for new modules and loads all that can be found.
        /// </summary>
        public void LoadAllModules()
        {
            ScanForNewModules();

            foreach (KeyValuePair<string, string> kvp in m_NonloadedModules)
                LoadModuleByAddress(kvp.Value);

            // Yes, I am particular about grammar.
            if (m_LoadedModules.Count != 1)
                Debug.Output("Loaded " + m_LoadedModules.Count + " modules.");
            else Debug.Output("Loaded " + m_LoadedModules.Count + " module.");
        }
        #endregion

        #region LoadModule
        /// <summary>
        /// Loads the module by the name specified that exists in the list of non-loaded modules.
        /// </summary>
        /// <param name="ModuleName">String containing the name of the module to be loaded.</param>
        public void LoadModule(string ModuleName)
        {
            if (m_NonloadedModules.ContainsKey(ModuleName))
                LoadModuleByAddress(m_NonloadedModules[ModuleName]);
            else Debug.Output("Following module not found in nonloaded modules list: " + ModuleName);
        }

        /// <summary>
        /// Loads the specified module by the address specified.
        /// </summary>
        /// <param name="ModuleName">Address of the module that needs to be loaded.</param>
        private void LoadModuleByAddress(string ModuleAddress)
        {
            Debug.Output("Looking for modules in: " + ModuleAddress);
            Assembly moduleAssembly = Assembly.LoadFrom(ModuleAddress);

            try
            {
                foreach (Type t in moduleAssembly.GetExportedTypes())
                {
                    if (t != null && !t.IsAbstract)
                    {
                        Type typeInterface = t.GetInterface(Globals.InterfaceName, true);
                        if (typeInterface != null)
                        {
                            IModule mod = (IModule)Activator.CreateInstance(moduleAssembly.GetType(t.ToString()));
                            if (m_LoadedModules.ContainsKey(mod.Name))
                            {
                                Debug.Output("Duplicate named modules found: " + mod.Name);
                                continue;
                            }

                            LoadedModule m = new LoadedModule(ModuleAddress);
                            m.Module = mod;
                            m.Thread = new Thread(new ThreadStart(m.Module.Start));

                            m_LoadedModules.Add(m.Module.Name, m);
                            m_NonloadedModules.Remove(m.Module.Name);

                            m.Module.OnLoad(this);
                            m.Thread.Start();

                            Debug.Output("Loaded module: " + m.Module.Name);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Debug.Output(e.ToString());
                Debug.Output("Failed to load dll: " + ModuleAddress);
            }
        }
        #endregion

        #region UnloadModule
        /// <summary>
        /// Unloads the specified module and removes it from the laoded modules list.
        /// </summary>
        /// <param name="ModuleName">Name of the module that needs to be unloaded.</param>
        public void UnloadModule(string ModuleName)
        {
            Debug.Output("Attempting to unload module: " + ModuleName);
            m_NonloadedModules.Add(ModuleName, m_LoadedModules[ModuleName].ModuleFileLocation);

            m_LoadedModules[ModuleName].Module.OnUnload();
            m_LoadedModules[ModuleName].Module.Dispose();
            m_LoadedModules[ModuleName].Module = null;
            m_LoadedModules[ModuleName].Thread.Abort();
            m_LoadedModules[ModuleName].Thread = null;
            m_LoadedModules[ModuleName] = null;
            m_LoadedModules.Remove(ModuleName);

            SpeechInput.UnloadGrammar(ModuleName);
            if (ModuleGrammarUnloaded != null)
                ModuleGrammarUnloaded(ModuleName);

            Debug.Output("Unloaded module: " + ModuleName);
        }

        /// <summary>
        /// Unloads the specified module.
        /// </summary>
        /// <param name="m">A reference to the IModule which is used as signature to verify the module's identity.</param>
        public void UnloadModule(IModule m)
        {
            if (m_LoadedModules[m.Name].Module.Equals(m))
                UnloadModule(m.Name);
        }
        #endregion

        #region LoadGrammar
        /// <summary>
        /// Loads the specified grammar file into the speech recognition engine.
        /// </summary>
        /// <param name="m">A reference to the IModule which is used as signature to verify the module's identity.</param>
        /// <param name="GrammarFileAddress"></param>
        /// <returns>Returns a string containing the </returns>
        public string LoadGrammar(IModule m, string GrammarFileAddress)
        {
            if (m_LoadedModules[m.Name].Module.Equals(m))
            {
                string rootName = SpeechInput.LoadGrammar(m.Name, m_LoadedModules[m.Name].ModuleBaseDirectory + @"\" + GrammarFileAddress);
                if (rootName != string.Empty)
                {
                    if (ModuleGrammarLoaded != null)
                        ModuleGrammarLoaded(m.Name);
                }
                else Debug.Output("Failed to load grammar for " + m.Name + ".");

                return rootName;
            }

            Debug.Output("Failed to confirm module identity while loading grammar for " + m.Name + ".");
            return string.Empty;
        }
        #endregion

        #region UnloadGrammar
        /// <summary>
        /// Unloads the grammar pertaining to this module.
        /// </summary>
        /// <param name="m">A reference to the IModule which is used as signature to verify the module's identity.</param>
        public void UnloadGrammar(IModule m)
        {
            if (m_LoadedModules[m.Name].Module.Equals(m))
            {
                SpeechInput.UnloadGrammar(m.Name);
                if (ModuleGrammarUnloaded != null)
                    ModuleGrammarUnloaded(m.Name);
                else Debug.Output("Failed to unload grammar for " + m.Name + ".");
            }
            else Debug.Output("Failed to confirm module identity while unloading grammar for " + m.Name + ".");
        }
        #endregion

        #region ReloadGrammar
        /// <summary>
        /// Reloads the grammar pertaining to this module.
        /// </summary>
        /// <param name="m">A reference to the IModule which is used as signature to verify the module's identity.</param>
        public void ReloadGrammar(IModule m)
        {
            if (m_LoadedModules[m.Name].Module.Equals(m))
            {
                SpeechInput.ReloadGrammar(m.Name);
                if (ModuleGrammarReloaded != null)
                    ModuleGrammarReloaded(m.Name);
                else Debug.Output("Failed to reload grammar for " + m.Name + ".");
            }

            Debug.Output("Failed to confirm module identity while reloading grammar for " + m.Name + ".");
        }
        #endregion

        #region SendMessage
        /// <summary>
        /// Sends a string message to the specified module.
        /// </summary>
        /// <param name="ToModuleName">Name of the module that will be messaged.</param>
        /// <param name="Message">String containing the message to be sent.</param>
        public void SendMessage(string ModuleName, string Message)
        {
            if (m_LoadedModules.ContainsKey(ModuleName))
                m_LoadedModules[ModuleName].Module.OnMessaged(Message);
        }
        #endregion

        #region DebugMessage
        /// <summary>
        /// Receive a debug statement from a module that can be handled by the IModuleHost.
        /// </summary>
        /// <param name="FromModuleName">Name of the module that the debug message is arriving from.</param>
        /// <param name="DebugMessage">String containing the debug message.</param>
        public void DebugMessage(string ModuleName, string DebugMessage)
        {
            Debug.Output(ModuleName + ": " + DebugMessage);
        }
        #endregion

        #region GetLoadedModuleNames
        /// <summary>
        /// Returns the list of all modules loaded currently.
        /// </summary>
        /// <returns>String array containing the names of all modules.</returns>
        public string[] GetLoadedModuleNames()
        {
            int i = 0;
            string[] names = new string[m_LoadedModules.Count];

            foreach (KeyValuePair<string, LoadedModule> kvp in m_LoadedModules)
                names[i++] = kvp.Key;

            return names;
        }
        #endregion

        #region GetNonloadedModuleNames
        /// <summary>
        /// Returns the list of all modules that are not currently loaded.
        /// </summary>
        /// <returns>String array containing the names of all modules not loaded.</returns>
        public string[] GetNonloadedModuleNames()
        {
            ScanForNewModules();

            int i = 0;
            string[] names = new string[m_NonloadedModules.Count];

            foreach (KeyValuePair<string, string> kvp in m_NonloadedModules)
                names[i++] = kvp.Key;

            return names;
        }
        #endregion

        #region Save and Load Settings
        /// <summary>
        /// Allows for the loading of settings from the specified path and utilizes the XMLSerializer.
        /// </summary>
        /// <param name="SettingsPath">String containing the path of the xml file containing
        /// the settings information.</param>
        /// <param name="t">Type defining the object that needs to be loaded.</param>
        /// <returns>Returns the object created from the settings found in SettingsPath.</returns>
        public object LoadSettings(string SettingsPath, Type t)
        {
            try
            {
                XmlSerializer serializer = new XmlSerializer(t);
                FileStream fs = new FileStream(SettingsPath, FileMode.Open);
                object o = serializer.Deserialize(fs);

                fs.Close();
                fs = null;
                serializer = null;

                return o;
            }
            catch (Exception)
            {
                return null;
            }
        }

        /// <summary>
        /// Allows for the saving of settings to the specified path and utilizes the XMLSerializer.
        /// </summary>
        /// <param name="SettingsPath">String containing the path of the xml file in which the
        /// class data will be stored.</param>
        /// <param name="t">Type defining the object that needs to be saved.</param>
        /// <param name="o">Object from which the data will be extrapolated and saved.</param>
        public void SaveSettings(string SettingsPath, Type t, object o)
        {
            XmlSerializer serializer = new XmlSerializer(t);
            TextWriter wr = new StreamWriter(SettingsPath);
            serializer.Serialize(wr, o);

            wr.Close();
            wr = null;
            serializer = null;
        }
        #endregion

        #region Cleanup
        /// <summary>
        /// Cleans up resources and unloads all modules.
        /// </summary>
        public void Cleanup()
        {
            List<string> keys = new List<string>();

            foreach (KeyValuePair<string, LoadedModule> kvp in m_LoadedModules)
            {
                keys.Add(kvp.Key);
            }

            foreach (string s in keys)
                UnloadModule(s);

            m_NonloadedModules.Clear();
            m_NonloadedModules = null;
            m_LoadedModules = null;
        }
        #endregion
    }
}
